Java Socket 之聊天室原理

相关文章:
Java Socket
Java Socket 聊天室原理
Java Socket 如何演化成 Tomcat

JAVA为基于TCP协议开发提供了相关API,具体代码如下。
客户端:

1
2
3
4
5
6
7
8
Socket client = new Socket("localhost", 9876); // 创建客户端,并指定服务器端地址和端口
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
dos.writeUTF("客户端向服务器端发送数据"); // 客户端向服务器发送数据
DataInputStream dis = new DataInputStream(client.getInputStream());
String msg = dis.readUTF(); // 客户端接受服务器端返回的数据
System.out.println(msg);
client.close();

服务端:

1
2
3
4
5
6
7
8
9
10
ServerSocket server = new ServerSocket(9876); // 创建服务器并指定端口
Socket socket = server.accept(); // 接受客户端连接(阻塞式)
DataInputStream dis = new DataInputStream(socket.getInputStream());
String msg = dis.readUTF(); // 读取客户端请求数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("服务器接受的数据:" + msg); // 服务器端返回客户端数据
dos.flush();
server.close();

上述代码中,只建立一次请求,为实现双方多次交互,则需要加入循环处理;而且客户端发送数据和接受数据都在一个线程中,意味着客户端不发送数据就接受不了服务端返回的数据,因此他们需要在不同的线程中处理各自的业务。服务器端同理。

客户端实现多线程

发送数据线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Send implements Runnable {
private BufferedReader console; // 测试时使用控制台输入信息
private DataOutputStream dos; // 输出流(发送数据)
private boolean isRunning = true; // 控制线程运行
public Send(Socket client) {
console = new BufferedReader(new InputStreamReader(System.in));
try {
dos = new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
end();
}
}
private String getMsgFromConsole() {
try { return console.readLine(); } catch (IOException e) {}
return "";
}
private void end() { // 出现异常则结束线程
isRunning = false;
CloseUtil.closeIO(console, dos);
}
private void send() { // 发送数据
String msg = getMsgFromConsole();
try {
if (null != msg && msg.length() > 0)
dos.writeUTF(msg);
} catch (IOException e) {
end();
}
}
public void run() { // 线程体
while (isRunning)
send();
}
}

接受数据线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Receive implements Runnable {
private DataInputStream dis; // 输入流(接受数据)
private boolean isRunning = true; // 控制线程运行
public Receive (Socket client) {
try {
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
end();
}
}
private void end() { // 出现异常则结束线程
isRunning = false;
CloseUtil.closeIO(dis);
}
private String receive() { // 接受数据
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
end();
}
return msg;
}
public void run() {
while (isRunning)
System.out.println(receive());
}
}

客户端只需开启Send、Receive两个线程:

1
2
3
Socket client = new Socket("localhost", 9876); // 创建客户端,并指定服务器端地址和端口
new Thread(new Send(client)).start();
new Thread(new Receive(client)).start();

服务端实现群聊

群聊,将服务端作为中转站,转发所有客户消息给其他客户(不需转给自己)。因此,服务端需要管理所有的连接通道,每个通道封装数据信息以及对应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Server {
// 保存连接通道
private List<ChatChannel> channels = new ArrayList<>();
// 启动服务
public void start() throws IOException {
@SuppressWarnings("resource")
ServerSocket server = new ServerSocket(9876);
while (true) {
Socket client = server.accept();
ChatChannel channel = new ChatChannel(client);
channels.add(channel);
new Thread(channel).start();
}
}
// 连接通道类
private class ChatChannel implements Runnable {
private DataInputStream dis;
private DataOutputStream dos;
private boolean isRunning = true;
public ChatChannel(Socket socket) {
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
end();
}
}
private String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
end();
}
return msg;
}
private void send(String msg) {
if (null == msg || msg.length() == 0)
return;
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
end();
}
}
private void end() {
CloseUtil.closeIO(dis, dos);
isRunning = false;
channels.remove(this);
}
@Override
public void run() {
while (isRunning) {
String msg = receive();
for (ChatChannel other : channels) {
if (other == this)
continue;
other.send(msg);
}
}
}
}
}

开启服务

1
new Server().start();

私聊

要找到对应的用户进行私聊,需要对每个客户端进行唯一性标示,即创建客户端的时候指定用户名/ID并发送给服务端,服务端在创建对应的ChatChannel时,保存获取到的用户名/ID信息。而私聊的代码实现就是将信息转发到指定的ChatChannel连接通道中。

按照面向对象编程的思想,用户信息、消息本体都要封装成一个个的对象。